﻿//*****************************************************************************
//
//  Copyright 2008 IT Cloud Company
//
//  This software is available under the Microsoft Public License.  Please
//  read the license agreement at http://opensource.org/licenses/ms-pl.html
//  before using the software.
//
//  Authors:
//    Don Song (don@itcloud.com)
//
//*****************************************************************************

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Threading;
using System.IO;

namespace HeadLinx.AD.Common
{
    public delegate void StatusMessageHandler(string message);

    public delegate void GenerateCompletionHandler(HeadLinxUser user, Exception ex);

    public class HeadLinxGenerator
    {
        private HeadLinxNameCollection _selectedUserNames = new HeadLinxNameCollection();

        private HeadLinxNameCollection _unresolvedUserNames = new HeadLinxNameCollection();

        private bool _abort = false;

        public event StatusMessageHandler StatusMessage;

        public event GenerateCompletionHandler GenerateCompletion;

        public HeadLinxNameCollection SelectedUserNames
        {
            get
            {
                return _selectedUserNames;
            }
        }

        public HeadLinxNameCollection UnresolvedUserNames
        {
            get
            {
                return _unresolvedUserNames;
            }
        }

        public bool ShowPeers
        {
            get;
            set;
        }

        public bool ShowDirectReports
        {
            get;
            set;
        }

        public bool ShowPhotos
        {
            get;
            set;
        }

        public HeadLinxPhotoCache PhotoCache
        {
            get;
            set;
        }

        public bool ForcePhotoCacheRefresh
        {
            get;
            set;
        }

        public int MaxCachedPhotoAgeDays
        {
            get;
            set;
        }

        public bool UseJpegPhoto
        {
            get;
            set;
        }

        /// <summary>
        /// Generates the tree asynchronously. Will fire the GenerateCompletion
        /// event when finished.
        /// </summary>
        public void QueryAsync(HeadLinxQueryType queryType)
        {
            _abort = false;
            ThreadPool.QueueUserWorkItem(new WaitCallback(InternalQueryAsync), queryType);
        }

        private void InternalQueryAsync(object state)
        {
            HeadLinxQueryType queryType = (HeadLinxQueryType)state;

            if (GenerateCompletion != null)
            {
                HeadLinxUser user = null;
                Exception ex = null;

                try
                {
                    user = this.Query(queryType);
                }
                catch (Exception e)
                {
                    ex = e;
                }

                GenerateCompletion(user, ex);
            }
        }

        public void CancelAsync()
        {
            _abort = true;
        }

        private void CheckAbort()
        {
            if (_abort)
            {
                throw new ApplicationException("User cancelled the operation.");
            }
        }

        /// <summary>
        /// Generates the tree synchronously.
        /// </summary>
        /// <returns></returns>
        public HeadLinxUser Query(HeadLinxQueryType queryType)
        {
            if (_selectedUserNames.Count == 0)
            {
                throw new ApplicationException("No user names selected.");
            }

            //
            //  Map used to store all users that make up the subtree linking
            //  together the users specified by the caller.
            //
            HeadLinxUserDictionary usersMap = new HeadLinxUserDictionary();

            //
            //  Map used to store users which were specified by the caller.
            //  This is used to distinguish selected users from discovered
            //  users that link them together.
            //
            HeadLinxUserDictionary selectedUsers = new HeadLinxUserDictionary();

            HeadLinxUser rootUser;
            bool minimumSpanning = false;

            //
            //  Enumerate names, look each one up in global catalog.
            //

            OutputStatus("Connecting to the AD global catalog...");

            CheckAbort();

            using (HeadLinxGlobalCatalog gc = new HeadLinxGlobalCatalog())
            {
                if (queryType == HeadLinxQueryType.Arbitrary)
                {
                    minimumSpanning = true;

                    foreach (string userNameRaw in _selectedUserNames)
                    {
                        string userName = userNameRaw.Trim();

                        if (String.IsNullOrEmpty(userName))
                        {
                            continue; // ignore empty name in list
                        }

                        CheckAbort();
                        SearchResult userResult = gc.FindUser(userName);

                        if (userResult == null)
                        {
                            // Could not resolve name, add it to unresolved list so
                            // caller knows about it and continue.
                            OutputStatus("Person {0} not found, ignoring.", userName);
                            _unresolvedUserNames.Add(userName);
                            continue;
                        }

                        OutputStatus("Retrieving information for {0}", userName);

                        //
                        //  Add the specified user to the map.
                        //
                        HeadLinxUser user;
                        usersMap.SetIfNotExist(userResult, out user);
                        selectedUsers[user.DistinguishedName] = user;

                        string userDN = user.DistinguishedName;

                        //
                        //  Add the specified user's managers to the map.
                        //

                        AddManagers(gc, user, usersMap);
                    }

                    //
                    //  Mark the users which were selected by the caller.  We need this
                    //  step because usersMap contains all users, including connecting
                    //  users that were not specified by caller.
                    //
                    foreach (string selectedUserDN in selectedUsers.Keys)
                    {
                        usersMap[selectedUserDN].SelectedUser = true;
                    }

                    rootUser = FindRootUser(usersMap, minimumSpanning);
                }
                else
                {
                    //
                    //  For these query types we only take first person specified.
                    //

                    string userName = _selectedUserNames[0].Trim();

                    CheckAbort();
                    SearchResult userResult = gc.FindUser(userName);

                    OutputStatus("Retrieving information for {0}", userName);

                    //
                    //  Add the specified user to the map.
                    //
                    HeadLinxUser user;
                    usersMap.SetIfNotExist(userResult, out user);
                    selectedUsers[user.DistinguishedName] = user;

                    //string userDN = user.DistinguishedName;

                    user.SelectedUser = true;

                    if (queryType == HeadLinxQueryType.DirectReports)
                    {
                        rootUser = user;

                        //
                        //  Add user's direct reports to the list.
                        //
                        CheckAbort();
                        HeadLinxNameCollection names = gc.FindDirectReports(user.DistinguishedName);

                        foreach (string directReportDN in names)
                        {
                            CheckAbort();
                            SearchResult result = gc.FindUserByDN(directReportDN);
                            HeadLinxUser directReportUser;
                            bool added = usersMap.SetIfNotExist(result, out directReportUser);
                            if (added)
                            {
                                OutputStatus("Found direct report " + directReportUser.FullName);
                            }

                            user.DirectReports.SetIfNotExist(directReportUser);
                        }
                    }
                    else if (queryType == HeadLinxQueryType.Peers)
                    {
                        //
                        //  Find the user's manager.
                        //
                        CheckAbort();
                        SearchResult managerResult = gc.FindUserByDN(user.ManagerDistinguishedName);
                        HeadLinxUser managerUser;

                        if (managerResult != null)
                        {
                            {
                                bool added = usersMap.SetIfNotExist(managerResult, out managerUser);

                                rootUser = managerUser;

                                if (added)
                                {
                                    OutputStatus("Found manager " + managerUser.FullName);
                                }
                            }

                            managerUser.DirectReports.SetIfNotExist(user);

                            //
                            //  Find the manager's direct reports (== the user's peers).
                            //
                            CheckAbort();
                            HeadLinxNameCollection names = gc.FindDirectReports(managerUser.DistinguishedName);

                            foreach (string directReportDN in names)
                            {
                                CheckAbort();
                                SearchResult result = gc.FindUserByDN(directReportDN);
                                HeadLinxUser peerUser;
                                bool added = usersMap.SetIfNotExist(result, out peerUser);
                                if (added)
                                {
                                    OutputStatus("Found peer " + peerUser.FullName);
                                }

                                managerUser.DirectReports.SetIfNotExist(peerUser);
                            }
                        }
                        else
                        {
                            // User with no manager means he is the head of the organization,
                            // simply return the user as the root with no peers.
                            rootUser = user;
                        }

                    }
                    else if (queryType == HeadLinxQueryType.Superiors)
                    {
                        //
                        //  Add the specified user's managers to the map.
                        //

                        rootUser = AddManagers(gc, user, usersMap);
                    }
                    else
                    {
                        throw new ApplicationException("Unknown query type.");
                    }
                }

            } // release global catalog object

            ////
            ////  Mark the users which were selected by the caller.  We need this
            ////  step because usersMap contains all users, including connecting
            ////  users that were not specified by caller.
            ////
            //foreach (string selectedUserDN in selectedUsers.Keys)
            //{
            //    usersMap[selectedUserDN].SelectedUser = true;
            //}

            //HeadLinxUser rootUser = FindRootUser(usersMap, minimumSpanning);

            if (this.ShowPhotos && (this.PhotoCache != null))
            {
                //
                //  Enumerate through the users in the tree and load
                //  their photos.
                //
                LoadUserPhoto_Recursive(rootUser, this.MaxCachedPhotoAgeDays);
            }

            return rootUser;
        }

        private void LoadUserPhoto_Recursive(HeadLinxUser user, int maxFileAgeDays)
        {
            CheckAbort();

            user.ThumbnailPhotoPath = LoadUserPhoto(user, maxFileAgeDays);

            foreach (HeadLinxUser directReport in user.DirectReports.Values)
            {
                LoadUserPhoto_Recursive(directReport, maxFileAgeDays);
            }
        }

        public void OutputStatus(string format, params object[] args)
        {
            string msg;

            if (args.Length == 0)
            {
                msg = format;
            }
            else
            {
                msg = String.Format(format, args);
            }

            Debug.WriteLine(msg);

            // Invoke delegates registered to receive status messages.
            if (StatusMessage != null)
            {
                StatusMessage(msg);
            }
        }

        private HeadLinxUser FindRootUser(HeadLinxUserDictionary usersMap, bool minimumSpanning)
        {
            HeadLinxUser rootUser = null;

            foreach (HeadLinxUser user in usersMap.Values)
            {
                if (String.IsNullOrEmpty(user.ManagerDistinguishedName) || !usersMap.ContainsKey(user.ManagerDistinguishedName))
                {
                    if (rootUser != null)
                    {
                        // Encountered more than one root in user tree.
                        throw new ApplicationException("One or more people in the list are not related to anyone.");
                    }

                    rootUser = user;

                    if (minimumSpanning)
                    {
                        //
                        //  Go down the hierarchy until we have the root of the
                        //  minimum spanning tree that contains all the users
                        //  selected by the caller, along with their direct reports
                        //  and peers if specified.
                        //

                        while (true)
                        {
                            if (!rootUser.SelectedUser && rootUser.DirectReports.Count == 1)
                            {
                                foreach (HeadLinxUser directReportUser in rootUser.DirectReports.Values)
                                {
                                    rootUser = directReportUser;
                                }
                            }
                            else
                            {
                                break;
                            }
                        }
                    }
                }
            }

            return rootUser;
        }

        private HeadLinxUser AddManagers(HeadLinxGlobalCatalog gc, HeadLinxUser user, HeadLinxUserDictionary targetMap)
        {
            //
            //  Iterate up the reporting hierarchy.
            //
            while (true)
            {
                CheckAbort();

                SearchResult managerResult = gc.FindUserByDN(user.ManagerDistinguishedName);

                if (managerResult != null)
                {
                    HeadLinxUser manager;

                    bool added = targetMap.SetIfNotExist(managerResult, out manager);

                    // Create link from manager to user.
                    manager.DirectReports.SetIfNotExist(user);

                    if (added)
                    {
                        OutputStatus("Found manager " + manager.FullName);

                        // Repeat iteration with manager as the user.
                        user = manager;
                    }
                    else
                    {
                        // Manager already exists in map, this means that the
                        // manager and everyone above him has already been
                        // added, so stop iterating.
                        break;
                    }
                }
                else
                {
                    // User does not have manager, stop iteration.
                    break;
                }
            }

            return user;
        }

        /// <summary>
        /// Loads the user's photo either from cache or from AD.
        /// </summary>
        /// <param name="user"></param>
        /// <param name="maxFileAgeDays">Maximum age of the cached photo file, in days.</param>
        /// <returns>Local path to the photo.</returns>
        private string LoadUserPhoto(HeadLinxUser user, int maxFileAgeDays)
        {
            Debug.Assert(this.PhotoCache != null);

            // Image name must be unique to a person.
            string imageName = String.Format("{0} ({1})", user.FullName, user.EmailAlias);

            string imagePath = null;

            if (!this.ForcePhotoCacheRefresh)
            {
                // Check if image exists in cache.
                imagePath = this.PhotoCache.GetImagePath(imageName, maxFileAgeDays);
            }

            if (imagePath != null)
            {
                OutputStatus("Using cached photo for " + user.FullName);
            }
            else
            {
                // Photo not found in cache, try to get it from AD.

                OutputStatus("Retrieving photo for " + user.FullName);

                byte[] imageData = HeadLinxUtils.TryGetUserThumbnailPhotoBytes(user.DistinguishedName, this.UseJpegPhoto);

                if (imageData == null)
                {
                    // Photo not found in AD, use a "not available" photo.

                    // NOTE: this isn't only for aesthetics, we need something
                    // in the cache to avoid querying AD again for this user's
                    // photo in future invocations.

                    imageData = PhotoNotAvailableImage.GetBytes();
                }

                if (imageData != null)
                {
                    // Cache it for next time.
                    imagePath = this.PhotoCache.SaveImageData(imageName, imageData);
                }
            }

            return imagePath;
        }
    }

    public class HeadLinxNameCollection : Collection<string>
    {
        /// <summary>
        /// Parses a list of names, separated by a comma or a semi-colon,
        /// and adds them to the collection.
        /// </summary>
        /// <param name="nameList"></param>
        public void ParseList(string nameList)
        {
            char[] delim = { ';', ',' };

            foreach (string name in nameList.Split(delim, StringSplitOptions.RemoveEmptyEntries))
            {
                this.Add(name);
            }
        }
    }
}
